package com.jgraves.WikiToSpeech;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.jgraves.WikiToSpeech.objects.Answer;
import com.jgraves.WikiToSpeech.objects.Dictionary;
import com.jgraves.WikiToSpeech.objects.DictionaryEntry;
import com.jgraves.WikiToSpeech.objects.Question;
import com.jgraves.WikiToSpeech.objects.Rule;
import com.jgraves.WikiToSpeech.objects.Sequence;

import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;

/** Parse the lines of the data source into a question sequence
 * @author johngraves
 * @param inputs 
 * <p>An array of strings containing the lines of the data source</p>
 * @return sequence 
 * <p>A list of questions</p>
 */
public class MakeSequence {	

	// UTILITY FUNCTION
	public static void print(Object obj){
		System.out.println(obj.toString());
	}

	// -----------------------
	public static Sequence makeSequence(Context context, String[] inputs, boolean fromZip) {

		Sequence sequence = new Sequence(null);
		Question question = new Question();
		Answer answer = new Answer();
		ArrayList<String> mStrings = new ArrayList<String>();
		ArrayList<String> mStringTypes = new ArrayList<String>();
		Map<String, Integer> mQuestionTagMap = new LinkedHashMap<String,Integer>();
		ArrayList<String> mRuleStrings = new ArrayList<String>();
		ArrayList<Rule> rules = new ArrayList<Rule>();

		// Check language preference and then script for language
		// The result determines the language used for the inputToken and nextToken 

		SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
		String languagePreference = prefs.getString("language", "UK");
		State.setLanguage(context, languagePreference);
		String inputToken = State.getInputToken();
		String nextToken = State.getNextToken();

		Boolean inQuote = false;
		Boolean inRule = false;
		Integer tripleQuoteAt = -1;
		ArrayList<String> ruleTypes = new ArrayList<String>();
		ruleTypes.add("_link");
		ruleTypes.add("_text");

		/** Pattern for matching (+/-)nn in response strings */
		final String matchString = "-+01234567890";

		// Clean out a #xXX; tag or &quot; tag
		// TODO use regular expressions or library
		for (String a : inputs) {
			if (-1<a.indexOf("&#x") && -1<a.indexOf(";", a.indexOf("&#x"))) {
				// Truncate string
				mStrings.add(a.substring(0,a.indexOf("&#x")));
			} else {
				mStrings.add(a);
			}   
		}		

		// Print mStrings to System.out
		//		for (String a : mStrings) {
		//			if (0==a.length()) {
		//				System.out.println("<blank>");
		//			} else {
		//				System.out.println(a);
		//			}   
		//		}	

		// First pass to determine types
		//	    Identify strings which contain new line only    ( type N )
		//	    #             or start with a hash # comment    ( type C )
		//	    #             or which contain ; or ;; markers  ( type A )
		//	    #             or start with rule indicators     ( type R )
		//	    #                [ rule type ]
		//	    #                [[ rule name ]]
		//	    #                re= or re =
		//	    #                example= or example =
		//	    #                reply= or reply =
		//	    #             or start with http:// or https:// ( type L )
		//	    #             or equal within [ ], parameter    ( type P )
		//	    #             or else mark as question          ( type Q )
		//      #                or question tag [ ]            ( type T )
		for (String mString : mStrings) {
			if (inQuote) {
				mStringTypes.add("R");
				tripleQuoteAt = mString.indexOf("\"\"\"");
				if (tripleQuoteAt != -1) {
					inQuote = false;
					inRule = false;
				}
			} else {
				if (0==mString.trim().length()) {
					mStringTypes.add("N");
				} else if (mString.startsWith("#")) {
					mStringTypes.add("C");
				} else if (mString.indexOf(";")>-1) {
					mStringTypes.add("A");
				} else if (mString.startsWith("[")) {
					if (mString.indexOf("=")>0) {
						mStringTypes.add("P");
					} else if (inRule) {
						mStringTypes.add("R");
					} else if (mString.substring(0, 2).equals("[[")) {
						// Second rule name
						mStringTypes.add("R");
						inRule = true;
					} else {
						// check content against list of rule types
						Integer bracketAt = mString.indexOf("]");
						if(bracketAt==-1){
							// right bracket missing?
							// make this a question so it can be seen and fixed
							mStringTypes.add("Q");
						} else {
							String maybeRuleType = mString.substring(1,bracketAt);
							if (ruleTypes.contains(maybeRuleType)){
								// It is a rule type
								mStringTypes.add("R");
								inRule = true;
							} else {
								// must be question tag 
								mStringTypes.add("T");
							}
						}
					}
				} else if (mString.startsWith("re=")) {
					mStringTypes.add("R");
				} else if (mString.startsWith("example=")) {
					mStringTypes.add("R");
				} else if (mString.startsWith("reply=")) {
					mStringTypes.add("R");
					tripleQuoteAt = mString.indexOf("\"\"\"");
					if(tripleQuoteAt == -1) {
						inRule = false;
					} else {
						inQuote = true;
						// test for SECOND (closing) triple quotes
						tripleQuoteAt = mString.indexOf("\"\"\"",tripleQuoteAt+2);
						if (tripleQuoteAt == -1) {
							inQuote = false;
							inRule = false;
						}
					}
				} else if (mString.startsWith("http://")) {
					mStringTypes.add("L");
				} else if (mString.startsWith("https://")) {
					mStringTypes.add("L");
				} else if (mString.startsWith("www.")) {
					mStringTypes.add("L");
				} else if (mString.startsWith("/sdcard/")) {
					mStringTypes.add("L");
				} else if (mString.startsWith("file:///sdcard/")) {
					mStringTypes.add("L");
				} else if (mString.trim().endsWith(".jpg")) {
					mStringTypes.add("L");
				} else if (mString.trim().endsWith(".JPG")) {
					mStringTypes.add("L");
				} else if (mString.trim().endsWith(".png")) {
					mStringTypes.add("L");
				} else if (mString.trim().endsWith(".PNG")) {
					mStringTypes.add("L");
				} else {
					mStringTypes.add("Q");
				}
			}
		}			
		// Print mStringTypes to System.out
		for (String a : mStringTypes) {
			if (0==a.length()) {
				System.out.println("<blank>");
			} else {
				System.out.println(a);
			}   
		}	

		// Regroup

		Integer onString = 0;
		Boolean mSlideshowModeOn = true; // new default for JPEG followed by text
		Boolean mFlashcardModeOn = false;
		Boolean mStickyBrowswerOn = false;
		Boolean mWebViewModeOn = true; // always on except for questions where it is not (end of webview voice over)

		// Initialize
		question = new Question();
		answer = new Answer();

		while (onString < mStrings.size()) {
			// check for tags and configuration overrides (which use =)
			String mString = mStrings.get(onString).trim();
			if (mString.startsWith("[") && mStringTypes.get(onString) != "A") {
				// This is a tag or parameter
				// Add prior question to sequence, if any
				if (0<question.getQuestionTexts().size()) {
					// Test whether next or final types is Q or R or L
					String nextOrFinalType = mStringTypes.get(Math.min( onString + 1, mStringTypes.size() - 1) );
					if (nextOrFinalType == "Q" || nextOrFinalType == "R" || nextOrFinalType == "L") {
						// add a default response if no subsequent response is provided
						if (0<question.getQuestionTexts().size() && ""==answer.getAnswerText()) {
							answer.setAnswerText(nextToken);
							answer.setAction(1);
							question.addAnswer(answer);
						}
						sequence.addQuestion(question);
						answer = new Answer();
						question = new Question();
					}
					// if not nextOrFinalType is not Q, R or L then blank is extra one or falls between Q and A
				}
			}

			// Parameters
			if ("P".equals(mStringTypes.get(onString))) {
				// This is a configuration override, split it on the "="
				String[] configParts = mString.split("[\\[\\]\\=]");
				if(configParts.length>2){
					String configItem = configParts[1].trim().toLowerCase();
					String configValue = configParts[2].trim().toLowerCase();
					print("Going in " + configItem + "=" + configValue);
					//					if ("photo".equals(configItem)) {
					//						photos.add(configValue);
					//					}
					if ("stickybrowser".equals(configItem)) {
						mStickyBrowswerOn = parseConfigValue(configValue);
						print("sticky is: "+mStickyBrowswerOn);
					}
					if ("flashcard".equals(configItem)) {
						mFlashcardModeOn = parseConfigValue(configValue);
						print("flashcard is: "+mFlashcardModeOn);
					}
					if ("slideshow".equals(configItem)) {
						mSlideshowModeOn = parseConfigValue(configValue);
						print("slideshow is: "+mSlideshowModeOn);
					}
					if ("language".equals(configItem)) {
						State.setLanguage(context, configValue);
						inputToken = State.getInputToken();
						nextToken = State.getNextToken();
					}
					if ("webview".equals(configItem)) {
						// NOTE: Value holds opposite of the hideWebView flag in Question object
						// When webview=false, hideWebView=true
						mWebViewModeOn = parseConfigValue(configValue);
						print("webview is: "+mWebViewModeOn);
					}
				}
			}

			// Add question tag
			if ("T".equals(mStringTypes.get(onString))) {
				String[] questionTagParts = mString.split("[\\[\\]]");
				if (questionTagParts.length>1){
					question.setTag(questionTagParts[1]);
				}
			}
			if ("Q".equals(mStringTypes.get(onString))) {
				// Look for any tokens marked by $ or ${xxx} and add them to the dictionary
				if(mString.indexOf("$")>0){
					Pattern p = Pattern.compile("\\$\\{?(\\w*)[\\}\\s]?"); // {?(\\w*)[}\\s]
					Matcher m = p.matcher(mString);
					while (m.find()) { // Find each match in turn
						String token = m.group(0).replaceAll("[\\$\\{\\}]",""); 
						// Check if already in dictionary
						if (null==State.getDictionary()) {
							Dictionary dictionary = new Dictionary();
							State.setDictionary(dictionary);
						}
						if (!State.getDictionary().contains(token)){
							DictionaryEntry de = new DictionaryEntry();
							de.setToken(token);
							State.getDictionary().addEntry(de);
						}
					}
				}
				if(!mWebViewModeOn){
					question.setHideWebView(true);
					mWebViewModeOn=true;
				}

				// if in flashcard mode, next line needs to be checked to be Q as well, 
				// although it is really an answer.
				// Both lines will be consumed.
				print("flashcard mode is now: " + mFlashcardModeOn);
				print("slideshow mode is now: " + mSlideshowModeOn);
				if (mFlashcardModeOn && 
						onString < mStringTypes.size() - 1 &&
						"Q"==mStringTypes.get(onString + 1)){
					// first add current Q 
					question.addQuestionText(mStrings.get(onString).trim());
					// make this into a flashcard answer
					answer.setAnswerText("[flashcard]");
					// add next Q as response to answer
					answer.setResponseText(mStrings.get(onString + 1).trim());
					// and skip it in next pass through loop
					onString += 1;
					answer.setAction(1);
					answer.setInput(true);
					question.addAnswer(answer);
				} 
				// check next line - if it is a Link, this Q is really an A and both lines are consumed			
				else if ((onString < mStrings.size() - 1) && 
						("L".equals(mStringTypes.get(onString + 1)))) {
					String linkString = mStrings.get(onString + 1);
					addCleanLink(answer, linkString);
					answer.setAnswerText("[" + mString + "]");
					answer.setAction(1);
					question.addAnswer(answer);
					answer = new Answer();
					onString += 1; // this is an EXTRA + 1 
				} 
				else if (mString.indexOf("]") > 1) {
					String[] questionTagParts = mString.split("[\\[\\]]");
					if (questionTagParts.length>1){
						question.setTag(questionTagParts[1]);
					}
				}
				else {
					question.addQuestionText(mString);
				}
			} else {

				if ("A".equals(mStringTypes.get(onString))) {
					answer = new Answer();
					// Answers (before ;) - possibly including links, input or next
					if (mString.startsWith("[")) {
						Integer spaceAt = mString.indexOf(" ");
						if (mString.startsWith(inputToken)) {
							answer.setAnswerText(inputToken);
							answer.setInput(true);
						} else
							if (mString.startsWith(nextToken)) {
								answer.setAnswerText(nextToken);
							} else
								// check for link
								if (1<spaceAt) {
									String linkString = mString.substring(1,spaceAt);
									if (linkString.startsWith("http")) {
										answer.setAnswerSideLink(linkString);
									} else {
										String mDirectoryPreference = prefs.getString("directory", context.getResources().getString(R.string.filepath));
										answer.setAnswerSideLink(mDirectoryPreference + "/" + linkString);
									}
									Integer rightBracketAt = mString.indexOf("]");
									if (spaceAt<rightBracketAt) {
										answer.setAnswerText( "[" + mString.substring(spaceAt+1,rightBracketAt+1));
									}
								} 
					} else {
						answer.setAnswerText(mString.substring(0,mString.indexOf(";")));
					}

					// Responses (after ;) - need to sort out action and response
					String responseString = mString.substring(mString.indexOf(";")+1).trim();
					if (0<responseString.length()) {
						// check for numbers (including negative) that are in the string 
						// (so we just need to find the length of that numeric string)
						// limit to the first 4 characters
						String actionString = "";
						Integer actionStringLength = 0;
						for (Character c : responseString.substring(0,Math.min(responseString.length(), 4)).toCharArray()) {
							if (-1<matchString.indexOf(c.toString()) && 0==actionString.length()) {
								actionStringLength += 1;
							} else {
								actionString = "0";
							}
						}
						actionString = responseString.substring(0, actionStringLength);
						//					print("actionString: "+actionString);

						if (0<actionString.length() && "0"!=actionString) {
							answer.setAction(Integer.decode(actionString));
						} 
						// check for semicolons
						else if (responseString.startsWith(";")) {
							// action will be number of semicolons
							Integer semicolonCount = 0;
							for (Character c : responseString.toCharArray()) {
								if (c == ';') { 
									semicolonCount += 1;
								}
							}
							answer.setAction(semicolonCount);
						} 
						// check response for destination
						else if (responseString.startsWith("[") && !responseString.startsWith(nextToken)) {
							String mDestination = "";
							Integer rightBracketAt = responseString.indexOf("]");
							if (2<rightBracketAt) {
								// second pass will be needed to determine if this destination matches any tags
								mDestination = responseString.substring(1, rightBracketAt);
								// remove bracketed part of response (should not be spoken)
								responseString = responseString.substring(rightBracketAt + 1);
							}
							answer.setResponseSideLink(mDestination);
						} 

						// Work on response part
						// If first character is a space, just trim
						if (responseString.startsWith(" ")) {
							responseString = responseString.trim();
						} else {
							// strip off initial characters relating to action
							String matchStringForResponses = "; " + matchString;
							while (0<responseString.length()) {
								if (-1<matchStringForResponses.indexOf(responseString.substring(0,1))) {
									responseString = responseString.substring(1);
								} else {
									break;
								}
							}
							print("responseString: "+responseString);
							answer.setResponseText(responseString);
						}
					}
					question.addAnswer(answer);
				}	// end of "A"
				else if ("L".equals(mStringTypes.get(onString))) {
					if (mSlideshowModeOn && 
							onString < mStringTypes.size() - 1 &&
							"Q"==mStringTypes.get(onString + 1)){
						if(mStrings.get(onString).startsWith("http")){
							question.setQuestionLink(mStrings.get(onString).trim());
						} else {
							// Add path to files from preferences unless pulling from ZIP file
							if(fromZip){
								question.setQuestionLink(mStrings.get(onString).trim());
							} else {
								String mDirectoryPreference = prefs.getString("directory", context.getResources().getString(R.string.filepath));
								question.setQuestionLink(mDirectoryPreference+ "/" + mStrings.get(onString).trim());
							}
						}
					} else {
						// a stray link to be set up on own page with ... [Link]
						question.addQuestionText("...");
						answer.setAnswerText("[Link]");
						answer.setAction(1);
						addCleanLink(answer, mStrings.get(onString));
						sequence.addQuestion(question);
						question = new Question();
					}

				} // end of "L"
				else if ("R".equals(mStringTypes.get(onString))) {
					// Collect strings for later
					mRuleStrings.add(mStrings.get(onString));
				} // end of "R"

				// Put parts into a question and add to the sequence
				if ("N".equals(mStringTypes.get(onString))) {
					if (0<question.getQuestionTexts().size()) {
						// Test whether next or final types is Q or R or L
						String nextOrFinalType = mStringTypes.get(Math.min( onString + 1, mStringTypes.size() - 1) );
						if (nextOrFinalType == "Q" || nextOrFinalType == "R" || nextOrFinalType == "L") {
							// add a default response if no subsequent response is provided
							if (0<question.getQuestionTexts().size() && ""==answer.getAnswerText()) {
								answer.setAnswerText(nextToken);
								answer.setAction(1);
								question.addAnswer(answer);
							}
							sequence.addQuestion(question);
							answer = new Answer();
							question = new Question();
						}
						// if not nextOrFinalType is not Q, R or L then blank line is an extra one or it falls between Q and A
					}
				}
			}
			// Last one
			if ((onString >= mStrings.size() - 1) && (question.getQuestionTexts().size() > 0)) {
				if (""==answer.getAnswerText()) {
					answer.setAnswerText(nextToken);
					answer.setAction(1);
					question.addAnswer(answer);
				}
				sequence.addQuestion(question);
			}
			onString += 1;
		}

		// Convert Rule Strings to Rules
		onString = 0;
		String ruleType = "";
		Rule rule = new Rule();
		String[] ruleStringParts = null;
		while(onString < mRuleStrings.size()) {
			String ruleString = mRuleStrings.get(onString);
			if (ruleString.startsWith("[")){
				ruleStringParts = ruleString.split("[\\[\\]]");
				if (ruleString.substring(0,2).equals("[[")) {
					rule.setRuleName(ruleString.split("[\\[\\]]")[2]);
				} else {
					rule.setRuleType(ruleString.split("[\\[\\]]")[1]);
				}
			} else {
				if (ruleString.startsWith("r")){
					if (ruleString.startsWith("reply")) {

						ruleStringParts = ruleString.split("\"");
						if(ruleStringParts.length==2){;
						rule.setRuleResponse(ruleStringParts[1]);
						rules.add(rule);
						ruleType = rule.getRuleType();
						rule = new Rule();
						rule.setRuleType(ruleType);
						}

					} else {
						// re (regular expression)

						ruleStringParts = ruleString.split("\"");
						if(ruleStringParts.length==2){
							Pattern p = Pattern.compile(ruleStringParts[1]);
							rule.setRulePattern(p);
							rules.add(rule);
							ruleType = rule.getRuleType();
							rule = new Rule();
							rule.setRuleType(ruleType);
						}
					}
				} else {
					// example
					ruleStringParts = ruleString.split("[\\[\\]]");

					if(ruleStringParts.length>1){
						String exampleString = ".*("+ruleStringParts[1].toLowerCase()+").*";
						Pattern p = Pattern.compile(exampleString);
						rule.setRulePattern(p);
						rules.add(rule);
						ruleType = rule.getRuleType();
						rule = new Rule();
						rule.setRuleType(ruleType);
					}
				}
			}
			onString++;
		}
		sequence.setRules(rules);

		// Use rules to fill in questions to ask in State.dictionary
		if(null!=State.getDictionary()) {
			if(State.getDictionary().missingQuestions().size()>0) {
				for(DictionaryEntry de:State.getDictionary().missingQuestions()) {
					String tokenNeedingQuestion = de.getToken();
					for (Rule matchingRule:rules) {
						if(matchingRule.getRuleName().length()>1){
							if(matchingRule.getRuleName().substring(1).equals(tokenNeedingQuestion)){
								de.setQuestionToFill(matchingRule.getRuleResponse());
								State.getDictionary().updateEntry(de);
								break;
							}
						}
					}
				}
			}
		}

		// Second pass to fix actions for destinations where they match tags
		// First collect the tags
		mQuestionTagMap.clear();
		/** Counter for question tags */
		Integer onQuestionTag = 0;
		List<Question> questionsInSequence = sequence.getQuestions();
		for (Question q : questionsInSequence) {
			if (""!=q.getTag()) {
				mQuestionTagMap.put(q.getTag(), onQuestionTag);
				// for example, [section2] is found at question 5
			}
			onQuestionTag += 1;
		}

		// stash in sequence for user lookups
		sequence.setmQuestionTagMap(mQuestionTagMap);

		// Then check if a destination matches a tag and, if so, find the relative action 
		// (difference between the question being checked, onQuestionTag, and the location in the map of the tag)
		// and remove destination
		// for example, question 2, answer 1 has ;[section2] as a responseSideLink, so it should get an action of 3 (5 minus 2)
		onQuestionTag = 0;
		for (Question q : questionsInSequence) {
			if (null!=q.getAnswers()) {
				List<Answer> answers = q.getAnswers();
				for (Answer a : answers) {
					if (""!=a.getResponseSideLink() && mQuestionTagMap.containsKey(a.getResponseSideLink())) {
						a.setAction(mQuestionTagMap.get(a.getResponseSideLink())-onQuestionTag);
						a.setResponseSideLink("");
					}
				}
			}
			onQuestionTag += 1;
		}
		// build final question(s) with Contents if any
		if (!mQuestionTagMap.isEmpty()) {
			Integer lastQuestionNumber = sequence.getSize();
			Object[] sectionNames = mQuestionTagMap.keySet().toArray();
			Integer sectionNamesCount = sectionNames.length;
			Integer onSectionName = 0;
			for (Object section : sectionNames) {
				if (0==(onSectionName % 5)) {
					if (0!=onSectionName) {
						// add batch of 5 to sequence with More ...
						lastQuestionNumber += 1;
						answer.setAnswerText("More ...");
						answer.setAction(1);
						question.addAnswer(answer);
						answer = new Answer();
						sequence.addQuestion(question);
						question = new Question();
					} else {
						mQuestionTagMap.put("contents", lastQuestionNumber);
					}
					answer = new Answer();
					question = new Question();
					question.addQuestionText("Contents ...");
				}
				String item = (String) section;
				answer.setAnswerText(item);
				answer.setAction(mQuestionTagMap.get(item)-lastQuestionNumber);
				question.addAnswer(answer);
				answer = new Answer();
				if (onSectionName==sectionNamesCount-1) {
					// add last batch to sequence
					sequence.addQuestion(question);
				} 
				onSectionName += 1;
			}
		} else {
			// Add in Contents with Start link (to question 0)
			mQuestionTagMap.put("contents", sequence.getSize());
			question = new Question();
			question.addQuestionText("Contents ...");
			answer = new Answer();
			answer.setAnswerText("Start ...");
			answer.setAction(-sequence.getSize());
			question.addAnswer(answer);
			sequence.addQuestion(question);
		}

		// Collect and set rules



		State.setSequence(sequence);
		State.printSequence();
		State.printDictionary();
		State.clearQuestionsVisited();
		return sequence;
	} // end makeSequence()

	//	// ---------------------
	//	/** Pass along setting of sticky browser to subsequent question */
	//	private void questionAddSticky() {
	//		if (mStickyBrowswerOn) {
	//			sticky.add("1");
	//		} else {
	//			sticky.add("0");
	//		}
	//		question.add(sticky);
	//	}

	// ---------------------
	/** Clean up string containing link and set as answerSideLink */
	private static void addCleanLink(Answer answer, String linkString) {
		// TODO Async check of whether these links are working
		if (linkString.startsWith("file:///sdcard")) {
			linkString = linkString.substring(7); // trim off file:// and leave /sdcard
		} else if (linkString.startsWith("www.")) {
			linkString = "http://" + linkString;
		}
		answer.setAnswerSideLink(linkString);
	}

	// ---------------------
	/** Allow flags to be set with any of these strings: on/off, 1/0, TRUE or true/<anything else including blank=false>
	 * @param configValue
	 * @return boolean
	 */
	private static boolean parseConfigValue(String configValue) {
		configValue = configValue.toLowerCase().trim();
		if ("on".equals(configValue)) {
			return Boolean.TRUE;
		} else if ("off".equals(configValue)) {
			return Boolean.FALSE;
		} else if ("1".equals(configValue)) {
			return Boolean.TRUE;
		} else if ("0".equals(configValue)) {
			return Boolean.FALSE;
		} else {
			return Boolean.valueOf(configValue); // value must be string "true" case insensitive. Otherwise returns false.
		} 
	} // end parseConfigValue()
}
